package com.atom.plugin.core

import com.android.build.api.transform.*
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import com.atom.plugin.core.concurrent.Schedulers
import com.atom.plugin.core.ext.eachFileRecurse
import com.google.common.collect.Sets
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.plugins.PluginContainer
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.util.CheckClassAdapter
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutorService
import java.util.concurrent.Future
import java.util.function.BiConsumer
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream

/**
 * All rights Reserved, Designed By www.rongdasoft.com
 * @version V1.0
 * @Title: AbstractPlugin
 * @Description:
 * @author: wuyongzheng
 * @date: 2022/3/15
 * @Copyright: 2022/3/15 www.rongdasoft.com Inc. All rights reserved.
 */
abstract class AbstractPlugin<E : AbstractExtension> : Transform(), Plugin<Project> {

    protected lateinit var project: Project
    protected lateinit var extension: E

    private var isApp: Boolean = true
    private val executor = lazy { createExecutor() }

    abstract fun getExtensionName(): String

    abstract fun getExtensionClass(): Class<E>

    override fun apply(project: Project) {
        Log.init(project)
        Log.e("Apply ${getExtensionName()} \nDate ${Date()}")
        this.project = project
        val plugins: PluginContainer = project.plugins
        // 主要是负责检查当前的plugin是否在android环境使用
        val hasAppPlugin: Boolean = plugins.hasPlugin(AppPlugin::class.java)
        hasAppPlugin.isTrue {
            val extensions: ExtensionContainer = project.extensions
            val appExtension = extensions.getByType(BaseExtension::class.java)
            this.isApp = appExtension is AppExtension
            this.extension = extensions.create(getExtensionName(), getExtensionClass())
                ?: this.getExtensionClass().newInstance()
            appExtension.registerTransform(this)
            project.afterEvaluate { p ->
                if (appExtension is AppExtension) {
                    afterEvaluate(p, appExtension)
                }
            }
        }
    }

    protected open fun afterEvaluate(project: Project, app: AppExtension) {
        Log.e("AfterEvaluate ${getExtensionName()} \njava version = ${System.getProperty("java.version")} \nextension = ${extension}")
    }

    override fun getName(): String = getExtensionName()

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> =
        TransformManager.CONTENT_CLASS

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return if (isApp) {
            TransformManager.SCOPE_FULL_PROJECT
        } else {
            Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT)
        }
    }

    override fun isIncremental(): Boolean = true

    override fun transform(transformInvocation: TransformInvocation?) {
        super.transform(transformInvocation)
        if (transformInvocation == null) {
            return
        }
        this.extension.also { e ->
            Log.setEnable(e.enableLog)
            Log.setAbort(e.abortError)
            try {
                val start = System.currentTimeMillis()
                Log.w("┌──────────────────────────BeforeTransform──────────────────────────┐")
                Log.w("├ ProjectName  : ${project.name}")
                Log.w("├ ExtensionName: ${getExtensionName()}")
                Log.w("├ ProjectPath  : ${transformInvocation.context.path}")
                Log.w("├ BuildType    : ${transformInvocation.context.variantName}")
                Log.w("├ Incremental  : ${transformInvocation.isIncremental}")
                Log.w("├ Extension    : $e")
                Log.w("├ StartTime    : ${Date(start)}")
                Log.w("└───────────────────────────────────────────────────────────────────┘")
                runTransform(transformInvocation)
                val end = System.currentTimeMillis()
                Log.w("┌───────────────────────────AfterTransform──────────────────────────┐")
                Log.w("├ EndTime      : ${Date(end)}")
                Log.w("├ ConsumeTime  : ${(end - start)}ms")
                Log.w("└───────────────────────────────────────────────────────────────────┘")
            } catch (e: Throwable) {
                Log.e("transform error" + e.localizedMessage)
                e.printStackTrace()
                Log.fatal(e)
            }
        }
    }

    private fun runTransform(transformInvocation: TransformInvocation) {
        val isIncremental = transformInvocation.isIncremental
        Log.e("|----runTransform Incremental=[${isIncremental}]")
        //如果非增量，则清空旧的输出内容
        if (!isIncremental) {
            try {
                transformInvocation.outputProvider.deleteAll()
            } catch (e: Exception) {
                Log.e("|----runTransform runTransform Init Error=[${e.localizedMessage}]")
                Log.fatal(e)
            }
        }
        val futures = LinkedList<Future<*>>()
        Log.e("|----transformInvocation.inputs [${transformInvocation.inputs.size}]--------------------------------")
        var checkError = false
        val errorCallback = { error: Throwable ->
            checkError = true
            System.err.println("Trace Output ERROR: " + error.message)
        }
        transformInvocation.inputs.forEach { transformInput ->
            Log.e("----jarInputs[${transformInput.jarInputs.size}]----------------------------")
            transformInput.jarInputs.forEach { jarInput ->
                futures.add(executor.value.submit {
                    eachJar(
                        transformInvocation,
                        isIncremental,
                        jarInput,
                        errorCallback
                    )
                })
            }
            Log.e("----directoryInputs[${transformInput.directoryInputs.size}]----------------------------")
            transformInput.directoryInputs.forEach { directoryInput ->
                futures.add(executor.value.submit {
                    eachDir(
                        transformInvocation,
                        isIncremental,
                        directoryInput,
                        errorCallback
                    )
                })
            }
        }
        Log.e("|----transformInvocation user ---------------------------------")
        futures.add(executor.value.submit {
            if (extension.enableUse) {
                onRunTransform(transformInvocation)
            }
        })
        Log.e("|----transformInvocation task execute Task [start] , size = ${futures.size} ")
        for (future in futures) {
            future.get()
        }
        if (checkError) {
            throw IllegalArgumentException("something wrong with trace, see detail log before")
        }
        futures.clear()
        Log.e("|----transformInvocation task execute Task [end]")
        onFinishTransform(transformInvocation)
    }

    protected open fun createExecutor(): ExecutorService {
        return Schedulers.io()
    }

    protected open fun onRunTransform(
        transformInvocation: TransformInvocation,
    ) {

    }

    protected open fun onFinishTransform(transformInvocation: TransformInvocation) {

    }

    private fun eachDir(
        transformInvocation: TransformInvocation,
        isIncremental: Boolean,
        directoryInput: DirectoryInput,
        checkError: (Throwable) -> Unit
    ) {
        val dest = transformInvocation.outputProvider.getContentLocation(
            directoryInput.name,
            directoryInput.contentTypes,
            directoryInput.scopes,
            Format.DIRECTORY
        )
        //存在 强制删除
        if (dest.exists()) {
            FileUtils.forceDelete(dest)
        } else {
            FileUtils.forceMkdir(dest)
        }
        // 检查是否需要过滤Dir
        if (isFilterDir(directoryInput)) {
            FileUtils.copyDirectory(directoryInput.file, dest)
            return
        }
        val biConsumer =
            BiConsumer { file: File, status: Status ->
                try {
                    // D:\app_git_android\demo_asm\test-kapt-ksp\app\build\intermediates\javac\debug\classes
                    val srcDirPath = directoryInput.file.absolutePath
                    // D:\app_git_android\demo_asm\test-kapt-ksp\app\build\intermediates\transforms\logPlugin\debug\47
                    val destDirPath = dest.absolutePath
                    // D:\app_git_android\demo_asm\test-kapt-ksp\app\build\intermediates\javac\debug\classes\com\atom\test\annotation\BuildConfig.class
                    val srcFilePath = file.absolutePath
                    // D:\app_git_android\demo_asm\test-kapt-ksp\app\build\intermediates\transforms\logPlugin\debug\47\com\atom\test\annotation\BuildConfig.class
                    val destFile = if (srcFilePath.contains(srcDirPath)) {
                        File(srcFilePath.replace(srcDirPath, destDirPath))
                    } else { // if not contains, changedFileOutput should be modify, else when we read and write the same file, the jar would be empty
                        File(destDirPath, file.name)
                    }
                    when (status) {
                        Status.NOTCHANGED -> {}
                        Status.REMOVED -> if (destFile.exists()) {
                            FileUtils.forceDelete(destFile)
                        }
                        Status.ADDED, Status.CHANGED -> {
                            parseDirToFile(
                                directoryInput.file,
                                file,
                                destFile, checkError
                            )
                        }
                    }
                } catch (e: IOException) {
                    Log.e("foreach Dir error 1 ${e}")
                }
            }
        //当前是否是增量编译
        if (isIncremental) {
            directoryInput.changedFiles.forEach(biConsumer)
        } else {
            eachFileRecurse(directoryInput.file) { file ->
                biConsumer.accept(
                    file,
                    Status.ADDED
                )
            }
        }
    }

    private fun eachJar(
        transformInvocation: TransformInvocation,
        isIncremental: Boolean,
        jarInput: JarInput,
        checkError: (Throwable) -> Unit
    ) {
        //File temDir = transformInvocation.getContext().getTemporaryDir();
        val createJarOutputName = createJarOutputName(jarInput)
        val dest = transformInvocation.outputProvider.getContentLocation(
            createJarOutputName,
            jarInput.contentTypes,
            jarInput.scopes,
            Format.JAR
        )
        //存在 强制删除
        if (dest.exists()) {
            FileUtils.forceDelete(dest)
        } else {
            FileUtils.forceMkdirParent(dest)
        }
        // 检查是否需要过滤jar
        if (isFilterJar(jarInput)) {
            FileUtils.copyFile(jarInput.file, dest)
            return
        }
        val status: Status = if (isIncremental) {
            jarInput.status
        } else {
            Status.ADDED
        }
        when (status) {
            Status.NOTCHANGED -> {}
            Status.REMOVED -> if (dest.exists()) {
                FileUtils.forceDelete(dest)
            }
            Status.ADDED, Status.CHANGED -> {
                parseJarToFile(jarInput.file, dest, checkError)
            }
        }
    }

    protected open fun isFilterJar(jarFile: JarInput): Boolean {
        return false
    }

    protected open fun isFilterDir(dirFile: DirectoryInput): Boolean {
        return false
    }

    /**
     * 判断某个class文件是否能够进行ASM
     * Params : name 包名加文件名 eg:com/atom/plugin/core/AbstractPlugin.kt
     *        : path 文件的绝对路径
     *        : isJar 该文件来自app还是jar
     */
    protected open fun isTransformEntry(name: String, path: String, isJar: Boolean): Boolean {
        if (inputTypes.isEmpty()) return false
        return if (inputTypes.size == 1 && inputTypes == TransformManager.CONTENT_CLASS) {
            //如果只关注class，则过滤
            name.endsWith(".class")
        } else {
            //关注除了class其他的，由插件自行判断
            true
        }
    }

    // 是否移除jar 内部的 entry
    protected open fun isRemoveJarEntry(jarFile: JarFile, entry: JarEntry): Boolean {
        //Log.e("isRemoveJarEntry > ${jarFile.name} , ${entry.name}")
        return false
    }

    // 是否移除Dir 内部的 entry
    protected open fun isRemoveDirEntry(rootParentFile: File, entryFile: File): Boolean {
        return false
    }

    @Throws(IOException::class)
    private fun parseJarToFile(
        inputFile: File, outputFile: File,
        errorCallback: (Throwable) -> Unit
    ) {
        try {
            // 装换为jarfile
            val jarFile = JarFile(inputFile)
            val jarOutputStream = JarOutputStream(FileOutputStream(outputFile))
            // 获取并遍历
            val enumeration = jarFile.entries()
            while (enumeration.hasMoreElements()) {
                val entry = enumeration.nextElement()
                val name = entry.name // 打印的是每一个class文件
                if (isRemoveJarEntry(jarFile, entry)) {
                    continue
                }
                // 创建输出的jar.entry文件
                val outJarEntry = JarEntry(entry)
                jarOutputStream.putNextEntry(outJarEntry)
                var outputEntryClassBytes: ByteArray? = null
                val inputEntryClassBytes = IOUtils.toByteArray(jarFile.getInputStream(entry))
                if (isTransformEntry(name, inputFile.absolutePath, true)) {
                    outputEntryClassBytes = try {
                        if (extension.enableUse) {
                            transformJar(inputEntryClassBytes, entry, inputFile, outputFile)
                        } else {
                            inputEntryClassBytes
                        }
                    } catch (e: Throwable) {
                        Log.e("parseJarToFile ${e.localizedMessage}")
                        inputEntryClassBytes
                    }
                    if (extension.checkClass) {
                        try {
                            val r = ClassReader(outputEntryClassBytes)
                            val w = ClassWriter(0)
                            val v: ClassVisitor = CheckClassAdapter(w)
                            r.accept(v, ClassReader.EXPAND_FRAMES)
                        } catch (e: Throwable) {
                            errorCallback.invoke(Throwable("Trace Jar Output ERROR: " + e.message + ", " + outJarEntry.name))
                        }
                    }
                }
                outputEntryClassBytes?.also {
                    jarOutputStream.write(it)
                } ?: also {
                    jarOutputStream.write(inputEntryClassBytes)
                }
                jarOutputStream.flush()
                jarOutputStream.closeEntry()
            }
            jarOutputStream.close()
            jarFile.close()
        } catch (e: Throwable) {
            Log.e("weaveSingleJarToFile Throwable $e")
        }
    }

    @Throws(IOException::class)
    private fun parseDirToFile(
        rootFile: File, inputFile: File, outputFile: File,
        errorCallback: (Throwable) -> Unit
    ) {
        if (isRemoveDirEntry(rootFile, inputFile)) {
            return
        }
        // 判断师傅可以进行转变
        if (isTransformEntry(
                inputFile.absolutePath.replace(rootFile.absolutePath + "\\", "").replace("\\", "/"),
                inputFile.absolutePath,
                false
            )
        ) {
            //  创建文件，如果文件存在则更新时间；如果不存在，创建一个空文件
            FileUtils.touch(outputFile)
            // 读取输入文件为byteArray
            val inputClassBytes = FileUtils.readFileToByteArray(inputFile)
            val outputClassBytes = if (extension.enableUse) {
                transformDir(inputClassBytes, inputFile, outputFile)
            } else {
                inputClassBytes
            }
            if (extension.checkClass) {
                try {
                    val r = ClassReader(outputClassBytes)
                    val w = ClassWriter(0)
                    val v: ClassVisitor = CheckClassAdapter(w)
                    r.accept(v, ClassReader.EXPAND_FRAMES)
                } catch (e: Throwable) {
                    errorCallback.invoke(Throwable("Trace Dir Output ERROR: " + e.message + ", " + inputFile.absolutePath))
                }
            }
            FileUtils.writeByteArrayToFile(outputFile, outputClassBytes)
        } else {
            // 直接进行复制即可
            if (inputFile.isFile) {
                FileUtils.copyFile(inputFile, outputFile)
            }
        }
    }

    abstract fun transformDir(
        classBytes: ByteArray,
        inputFile: File,
        outputFile: File
    ): ByteArray

    abstract fun transformJar(
        classBytes: ByteArray,
        entry: JarEntry,
        inputFile: File,
        outputFile: File
    ): ByteArray


    private fun createJarOutputName(input: JarInput): String {
        var destName = input.name
        // 重新命名
        val hexName = DigestUtils.md5Hex(input.file.absolutePath).substring(0, 8)
        if (destName.endsWith(".jar")) {
            // 如果是.jar结尾，截取之前的字符串
            destName = destName.substring(0, destName.length - 4)
        }
        return destName + "_" + hexName
    }

}